LINQ is the standard on querying the data on .NET platform. DataObjects.Net provides full-featured LINQ API for querying persistent objects.
Session.Query property provides a set of members allowing you to query the database using LINQ.
Session.Query.All<T>() method is the most important one. It creates and returns a new IQueryable<T> instance that allows you to get all entities assignable to type T.
var products = session.Query.All<Product>();
foreach (var product in products) {
...
}
**Note**
In this chapter we'll often use term *query* as a synonym for
``IQueryable`` instance.
Unless session uses client profile you’ll need an active transaction to enumerate query results.
using (var session = domain.OpenSession()) {
using (var t = session.OpenTransaction()) {
var articles = from a in session.Query.All<Article>() select a;
foreach (var article in articles) {
...
}
}
}
Each query is implicitly bound to a session that created it. To enumerate query result this session should not be disposed at the moment of query execution. The following example demostrates incorrect usage of query.
IQueryable<Product> products;
using (var session = domain.OpenSession()) {
using (var t = session.OpenTransaction()) {
products = session.Query.All<Product>();
}
}
// Wrong! Session that created products queryable is already disposed here.
foreach (var p in products) {
}
The object returned by Session.Query.All() method implements IQueryable interface. This means you could use standard LINQ facilities to construct more complex queries.
var result =
from e in session.Query.All<Employee>()
where e.FullName!=null
select e;
from p in session.Query.All<Product>()
select p.UnitsInStock * p.UnitPrice
Multiple IQueryable<T> instances could be used in the same query expression:
var result =
from c in session.Query.All<Customer>()
join o in session.Query.All<Order>()
on c equals o.Customer into ords
join e in session.Query.All<Employee>()
on c.Address.City equals e.Address.City into emps
select new {ords = ords.Count(), emps = emps.Count()};
Session.Query provides two method groups allowing fetch a single entity:
These methods are pseudo-LINQ. They generally follow semantics of the standard Single and SingleOrDefault extension methods. However some differences exist. Regular LINQ queries always perform a database query. Pseudo-LINQ methods first try to find entity in session cache. If entity is found there it is returned immediately. Otherwise database query is performed.
DataObjects.Net allows using many of base class library operators and methods in queries. Generally you can expect most of the members of the supported persistent types to work. Please see persistent fields section for information about supported types. All provided members are supported only for the specified types.
Standard C# operators: == != < <= > >= || && ! ?? ?: + - * /
IQueryable<Product> calculableFilter =
session.Query.All<Product>()
.Where(p => 100 + p.UnitPrice * p.UnitsInStock/100 >= 100);
IQueryable<decimal> calculableColumn =
session.Query.All<Product>()
.Select(p => 100 + p.UnitPrice * p.UnitsInStock/100);
IQueryable<Product> calculableFilter =
session.Query.All<Product>().Where(p =>
p.UnitPrice >= 100 && p.UnitPrice<=200 || !(p.ProductName=="Name"));
IQueryable<bool> calculableColumn =
session.Query.All<Product>().Select(p =>
p.UnitPrice >= 100 && p.UnitPrice<=200 || !(p.ProductName=="Name"));
Standard methods: Equals, ToString, IEquatable<T>.Equals, IComparable<T>.CompareTo.
Decimal methods: Add, Compare, Divide, Parse, Remainder, Subtract
Math type methods:
Nullable<T> properties: Value, HasValue
String methods:
DateTime:
DateOnly (only in NET6 assembly):
TimeOnly (only in NET6 assembly):
for the type due to existance of native type; PostgreSQL and SQLite have limited support as long as they lack native type which can store datetime with timezone):
TimeSpan:
EntitySet<T> implements IQueryable<T>, so it is possible to use it as query root:
Customer customer = GetCustomer();
EntitySet<Order> orders = customer.Orders;
var result =
from order in orders
where order.Freight > 100
select order;
GroupBy method returns IQueryable<IGrouping<TGroupingKey, TElement>>. Each IGrouping<TGroupingKey, TElement> implements IQueryable<TElement> as well, so it can be used as query root after explicit cast to IQueryable<TElement>. An alternative to explicit cast here is standard AsQueryable method – if source is queryable, it simply makes the cast we need:
IQueryable<IGrouping<decimal, Product>> groupings =
from product in session.Query.All<Product>()
group product by product.UnitPrice;
foreach (IGrouping<decimal, Product> grouping in groupings) {
Log.Info("Unit Price {0}", grouping.Key);
// Use IGrouping<TGroupingKey, TElement> as IQueryable<TElement>
var categories =
from product in grouping.AsQueryable()
select product.Category.CategoryName;
foreach (string category in categories)
Log.Info("Category {0}", category);
}
Subquery result implements IQueryable<T> as well:
IQueryable<IQueryable<Order>> result =
from customer in session.Query.All<Customer>()
select // Create subquery
from order in session.Query.All<Order>()
where order.Customer==customer
select order;
foreach (IQueryable<Order> orders in result) {
var subQueryCount = orders.Count(); // Execute subquery.
// ....
}
DataObjects.Net can materialize not just the objects of built-in types (Entity, Structure, etc.), but objects of generally any type:
var orders = from customer in session.Query.All<Customer>()
select new CustomOrder(customer.Id) {
Country = customer.Address.Country,
City = customer.Address.City
};
As you see, here we create a custom, non-persistent CustomerOrder type and set its two properties – in fact, by doing this we’re mapping these properties to data participating in query. DataObjects.Net maintains such mapping information for the whole query, so it is possible to use properties of custom materialized objects further in the query:
var filteredOrders =
from order in
from customer in session.Query.All<Customer>()
select new CustomOrder(customer.Id) {
Country = customer.Address.Country,
City = customer.Address.City
}
where order.Country=="Russia"
select order;
DataObjects.Net allow you to mix operations on local collections and server-side data in queries. Local collections are objects of IEnumerable<T>. While executing such a query, DataObjects.Net either creates a temporary table, persists the data extracted from local objects into it and uses it in SQL statement as any other table, or injects local collection content into generated SQL (e.g. into in IN (...)). The last case is used if collection is small enough and usage of IN or boolean expression with collection elements is possible.
// Local collection of decimals.
var freights = new List<decimal> {1, 234, 334, 554, 343, 753};
var result = from storageOrders in session.Query.All<Order>()
join freight in freights on storageOrders.Freight equals freight
select storageOrders;
Moreover, DataObjects.Net provides Session.Query.Store<T> method – it converts IEnumerable<T> to IQueryable<T>, so it allows use local collections as query roots. Such queryables support all the operations you can do with any other DataObjects.Net queryable.
As it was mentioned, underlying IEnumerable<T> will be persisted to storage as temporary table on execution of such query.
var freights = new List<decimal> {1, 234, 554, 343};
var localCollectionQuery = session.Query.Store<decimal>(freights);
// It's possible to use params T[]:
// var localCollectionQuery = session.Query.Store<decimal>(1, 234, 554, 343);
var result = from freight in localCollectionQuery
join storageOrders in session.Query.All<Order>()
on freight equals storageOrders.Freight
select storageOrders;
DataObjects.Net extends IQueryable<T> type with a set of useful methods:
LeftJoin simplifies writing of left joins. The syntax is exactly the same as of standard Join method, the only difference is the way query is executed.
var territories = session.Query.All<Territory>();
var regions = session.Query.All<Region>();
var result = territories.LeftJoin(
regions,
territory => territory.Region.Id,
region => region.Id,
(territory, region) => new {
territory.TerritoryDescription,
region.RegionDescription
});
Note that there is another way to write left join using standard LINQ syntax:
var result =
from order in session.Query.All<Order>()
join customer in session.Query.All<Customer>()
on order.Customer equals customer into oc
from joinedCustomer in oc.DefaultIfEmpty()
select new {
CustomerId = joinedCustomer.Id,
joinedCustomer.CompanyName,
joinedCustomer.Address.Country
};
DataObjects.Net LINQ translator recognizes this way. But we decided to add LeftJoin to make the syntax more explicit.
Lock adds locking hints to the underlying query:
var customers = session.Query.All<Customer>()
.Where(c => c.Key == key)
.Lock(LockMode.Update, LockBehavior.Skip);
In checks if value is contained in specified list of values. In fact, it is an alternative syntax for Contains. The following two queries are equal:
var customers = from c in session.Query.All<Customer>()
where c.Id.In("FISSA", "PARIS")
select c.Orders;
var customers = from c in session.Query.All<Customer>()
where new List<string> {"FISSA", "PARIS"}.Contains(c.Id)
select c.Orders;
ExecuteAsync runs query to database asynchronously. For IQueryable<T> which are not created by DataObjects.Net (e.g. Enumerable.Range(0, 12).AsQueryable()) it returns completed task.
var customers = await session.Query.All<Customer>()
.Where(c => c.Key == key).ExecuteAsync();
Note that though it returns Task<IEnumerable<Customer>> it should not be executed in parallel with another action with persistent objects (creation, getting or setting field values, removing, executing another synchronous or asynchronous query, etc.), so we strongly recommend you to await results right away. A wrong usage example:
var customersTask = session.Query.All<Customer>()
.Where(c => c.Key == key).ExecuteAsync();
var products = await session.Query.All<Product>().
.Where(p => p.Name.Contains("Beans")).ExecuteAsync();
var customers = await customers;
As an exception you can postpone awaiting to do some work which is not connected to DataObjects.Net, for instance:
- var customersTask = session.Query.All<Customer>()
- .Where(c => c.Key == key).ExecuteAsync();
- using(var streamReader = new StreamReader(fileName, System.Text.Encoding.UTF8)) {
- var fileContent = await streamReader.ReadToEndAsync(); streamReader.Close(); await ProcessContent(fileContent);
}
var customers = await customersTask;
Query compilation takes time. If performance is really important, it is possible to compile the query once and execute it multiple times with different parameters afterward. This could decrease average execution time of a query by up to 10 times.
Session.Query.Execute method allows to compile and run such compiled query:
private IEnumerable<Employee> GetEmployees(DateTime date)
{
return session.Query.Execute(q =>
from employee in q.All<Employee>()
where employee.BirthDate < date
select employee);
}
Only the first call to GetEmployees method will cause query compilation. Subsequent execution of this method will lead to execution of previously compiled query, but with new parameters.
There is asynchronous version as well:
private async Task<IEnumerable<Employee>> GetEmployeesAsync(DateTime date)
{
return await session.Query.ExecuteAsync(q =>
from employee in q.All<Employee>()
where employee.BirthDate < date
select employee);
}
Unfortunately, LINQ has some limitation related to Take, Skip, ElementAt, ElementAtOrDefault in such scenarios: all of them allow to pass only an integer number as parameter, but not an expression. This number is always “encoded” as constant in LINQ expression our translator gets, thus there is no chance to change the value after the query has been compiled.
But DataObjects.Net provides its own Take, Skip, ElementAt, ElementAtOrDefault extension methods with parameter of type Expression<Func<int>> instead of int. This solution allows to work around this issue.
The first method of the following example throws an exception, but the second one works properly.
private IEnumerable<Customer> TakeCustomersIncorrect(int amount)
{
return session.Query.Execute(q => q.All<Customer>().Take(amount));
}
private IEnumerable<Customer> TakeCustomersCorrect(int amount)
{
return session.Query.Execute(q => q.All<Customer>().Take(() => amount));
}
The following IQueryable extension methods are fully supported by DataObjects.Net LINQ translator except the overloads that take IComparer or IEqualityComparer as an argument:
All Any AsQueryable Average Concat Contains Count Distinct
Except First FirstOrDefault GroupBy GroupJoin Intersect Join
LongCount Max Min OfType OrderBy OrderByDescending Reverse
Select SelectMany Single SingleOrDefault Sum
ThenBy ThenByDescending Union Where
Paging operations (ElementAt ElementAtOrDefault Take Skip) are supported in regular queries only. These methods are not supported in compiled queries. DataObjects.Net-specific overloads that work in compiled queries are available. These overloads require Func<int> argument instead of int and could be used in regular queries as well as compiled queries.
DefaultIfEmpty is supported in special scenario when it is used with SelectMany:
from customer in session.Query.All<Customer>()
from order in session.Query.All<Order>()
.Where(o => o.Customer==customer)
.DefaultIfEmpty()
select {customer, order}
DataObjects.Net does not support any LINQ method that takes IComparer or IEqualityComparer as an argument. Also the following methods are not supported:
Aggregate Cast TakeWhile SkipWhile SequenceEqual Last LastOrDefault
LINQ provides standard Join extension method that normally translates to SQL as inner join. The following query:
var result =
from product in session.Query.All<Product>()
join supplier in session.Query.All<Supplier>() on product.Supplier.Id
equals supplier.Id
select new {product.ProductName, supplier.ContactName, supplier.Phone};
will be translated to the following SQL:
SELECT [a].[ProductId],
[a].[TypeId],
[a].[ProductName],
[b].[SupplierId] AS [#a.SupplierId],
[b].[TypeId] AS [#a.TypeId],
[b].[ContactName] AS [#a.ContactName],
[b].[Phone] AS [#a.Phone]
FROM [dbo].[Products] [a]
INNER JOIN [dbo].[Suppliers] [b]
ON ([a].[Seller.SupplierId] = [b].[SupplierId])
LINQ does not provide standard method for left join, although it is possible to describe it using other query operations. But, as you’ll see below, the sequence you must write to describe left join is pretty complicated. DataObjects.Net provides its own LeftJoin extension method for IQueryable<T> to make left join syntax more clear.
The syntax of LeftJoin is the same as syntax for standard Join, but obviously, it can be used as extension method only:
var result = session.Query.All<Product>()
.LeftJoin(session.Query.All<Supplier>(),
product => product.Supplier.Id,
supplier => supplier.Id,
(product, supplier) => new {
product.ProductName,
supplier.ContactName,
supplier.Phone
});
And the result of the LeftJoin is the similar to standard Join but join is left instead of inner:
SELECT [a].[ProductId],
[a].[TypeId],
[a].[ProductName],
[b].[SupplierId] AS [#a.SupplierId],
[b].[TypeId] AS [#a.TypeId],
[b].[ContactName] AS [#a.ContactName],
[b].[Phone] AS [#a.Phone]
FROM [dbo].[Products] [a]
LEFT OUTER JOIN [dbo].[Suppliers] [b]
ON ([a].[Seller.SupplierId] = [b].[SupplierId])
As it was mentioned, LINQ does not provide standard LeftJoin method, but it is possible to define left join using GroupJoin, SelectMany and DefaultIfEmpty methods:
var result = from order in session.Query.All<Order>()
join customer in session.Query.All<Customer>()
on order.Customer equals customer into oc
from joinedCustomer in oc.DefaultIfEmpty()
select new {
CustomerId = joinedCustomer.Id,
joinedCustomer.CompanyName,
joinedCustomer.Address.Country
};
Translation result for this query is the same as for query with LeftJoin:
SELECT [a].[CustomerId] AS [#c.CustomerId],
[a].[TypeId] AS [#c.TypeId],
[a].[CompanyName] AS [#c.CompanyName],
[a].[Address.Country] AS [#c.Address.Country]
FROM [DBO].[ORDER] [b]
LEFT OUTER JOIN [DBO].[Customers] [a]
ON ([b].[Customer.CustomerId] = [a].[CustomerId])
Entity safe cast (as operator for Entity descendants) is translated with use of left join:
var result = session.Query.All<Product>()
.Select(product => product as DiscontinuedProduct);
SELECT [a].[ProductId] AS [#a.ProductId],
[a].[TypeId] AS [#a.TypeId],
[a].[ProductName] AS [#a.ProductName],
[a].[Seller.SupplierId] AS [#a.Seller.SupplierId],
[a].[Category.CategoryId] AS [#a.Category.CategoryId],
[a].[ProductType] AS [#a.ProductType],
[a].[UnitPrice] AS [#a.UnitPrice],
[a].[UnitsInStock] AS [#a.UnitsInStock],
[a].[UnitsOnOrder] AS [#a.UnitsOnOrder],
[a].[ReorderLevel] AS [#a.ReorderLevel],
[a].[QuantityPerUnit] AS [#a.QuantityPerUnit]
FROM [dbo].[Products] [b]
LEFT OUTER JOIN [dbo].[Products] [a]
ON ([b].[ProductId] = [a].[ProductId])
WHERE ([a].[TypeId] IN (107))
DataObjects.NET uses left join to provide access to fields of referenced Entity:
var result = session.Query.All<Product>().Select(p => p.Category.CategoryName);
SELECT [a].[CategoryId] AS [#a.CategoryId],
[a].[TypeId] AS [#a.TypeId],
[a].[CategoryName] AS [#a.CategoryName]
FROM [dbo].[Products] [b]
LEFT OUTER JOIN [dbo].[Categories] [a]
ON ([b].[Category.CategoryId] = [a].[CategoryId])
Joins are also used to gather the date from inheritance hierarchies. For example, if InheritanceSchema.ClassTable is used in a particular hierarchy, fields of any hierarchy root descendant are spanned over multiple tables there. So to gather the whole field set for a particular type, DataObjects.Net will use joins.
Let’s assume Employee is inherited form Person in hierarchy with InheritanceSchema.ClassTable. So the following query:
var employees = session.Query.All<Employee>();
will be translated with inner join of two tables:
SELECT [a].[Id],
[a].[TypeId],
[a].[Name],
[b].[Salary]
FROM (SELECT [c].[Id],
[c].[TypeId],
[c].[Salary]
FROM [dbo].[Employee] [c]) [b]
INNER JOIN (SELECT [d].[Id],
[d].[TypeId],
[d].[Name]
FROM [dbo].[Person] [d]) [a]
ON ([a].[Id] = [b].[Id]);
Subqueries are either expressions of IQueryable<T> type inside projections of primary query, or aggregates calculated over such expressions.
If subquery is a part of final projection (final .Select() call), it is executed separately for each query result item during enumeration of result of original query. So in this case:
var query = session.Query.All<Person>().Select(
employee => new {
employee,
Namesakes = session.Query.All<Person>()
.Where(person => person.Name==employee.Name)
});
// Enumerate query
foreach (var employeeData in query) {
// Enumerate each subquery element
foreach (Person namesake in employeeData.Namesakes) {
// Do something with employee, namesake
}
}
SELECT [a].[Id],
[a].[TypeId],
[a].[Name]
FROM [dbo].[Person] [a];
SELECT [a].[Id],
[a].[TypeId],
[a].[Name]
FROM [dbo].[Person] [a]
WHERE ([a].[Name] = 'John');
SELECT [a].[Id],
[a].[TypeId],
[a].[Name]
FROM [dbo].[Person] [a]
WHERE ([a].[Name] = 'Susan');
Highlighted part is SQL corresponding to the main query; a set of SELECTs below are subqueries performed for each item returned by the main query.
Similar subqueries are produced for GroupBy method results: IGrouping<TKey, TElement> is also treated as a subquery.
If subquery returns scalar result, it is evaluated as part of original SQL query. In particular, this happen if subquery method chain ends with: Contains Any All Sum Average Min Max First FirstOrDefault Single SingleOrDefault
The following example illustrates subquery with All method:
var result = session.Query.All<Customer>()
.Where(c => session.Query.All<Order>()
.Where(o => o.Customer==c)
.All(o => o.ShippingAddress.City==c.Address.City));
SELECT [a].[CustomerId],
[a].[TypeId],
[a].[CompanyName],
[a].[ContactName],
[a].[ContactTitle],
[a].[Address.StreetAddress],
[a].[Address.City],
[a].[Address.Region],
[a].[Address.PostalCode],
[a].[Address.Country],
[a].[Phone],
[a].[Fax]
FROM [dbo].[Customers] [a]
WHERE (NOT EXISTS(
SELECT *
FROM [dbo].[Order] [b]
WHERE (
([b].[Customer.Id] = [a].[CustomerId])
AND
(NOT ([b].[ShippingAddress.City] = [a].[Address.City])))
));
DataObjects.Net allows to build full-text indexes for domain entities and execute full-text queries against database. As of DataObjects.Net 4.6, the feature is implemented for SQL Server and PostgreSQL.
DataObjects.Net simplifies the task of creating full-text indexes by providing the corresponding FullText attribute, that should be applied to indexed fields. It requires one mandatory argument - the language which will be used to instruct database server to choose the appropriate resource for word breaking, stemming, and thesaurus and stopword removal as part of the query.
public class Beverage : Entity
{
[Field, Key]
public int Id { get; private set; }
[Field]
[FullText("English")]
public string Name { get; set; }
}
Endpoint for full-text queries is Session.Query.FreeText<TEntity>(string searchCriteria)` method. But instead of returning ``IQueryable<TEntity> it returns IQueryable<FullTextMatch<TEntity>>. FullTextMatch<TEntity> is a simple class containing 2 fields: Entity and its Rank according to full-text index and search criteria.
// Create some beverages
new Beverage(session) {
Name = "Sherbet Hawiian Punch"
};
new Beverage(session) {
Name = "Miami-Fusion Virgin Mojito"
};
new Beverage(session) {
Name = "Guava Ginger Punch"
};
// Query for 'punch guava' and order by rank desc
var matches = session.Query.FreeText<Beverage>("punch guava")
.OrderByDescending(i => i.Rank);
foreach (var match in matches) {
Console.WriteLine(match.Rank);
Console.WriteLine(match.Entity.Name);
}
// Limit the number of matches by some rank
var matches = session.Query.FreeText<Beverage>("punch guava")
.Where(i => i.Rank > 0.15)
.OrderByDescending(i => i.Rank);